原文:How does ‘kubectl exec’ work?
作者:Erkan Erol
上周五,一个同事问了我一个问题——如何使用 client-go 在 Pod 中执行命令。我答不出来,而且注意到我从来没想过 kubectl exec
的实现机制。我对这个问题有一点认识,但又不是很确定。我记下了这个题目,进行了一番探索,在阅读了大量博客、文档和代码之后,收获了很多知识。本文中我会分享这个过程中的理解和发现。
环境
我使用 https://github.com/ecomm-integration-ballerina/kubernetes-cluster 中的工具在我的 Macbook 上创建 Kubernetes 集群。缺省配置不允许运行 kubectl exec
,我在 Kubelet 配置中修改了 IP 地址,具体原因参见博客:Playing with kubeadm in Vagrant Machines。
1 | Any machine = my MacBook |
组件
kubectl exec
进程:在我们运行kubectl exec
时,会启动一个进程。可以在任何一台能够访问到 Kubernetes API Server 的机器上运行该命令。api-server
:运行在 Master 上,提供开放的 Kubernetes API,它是 Kubernetes 控制平面的前端。kubelet
:在集群所有节点上都会运行这个进程,它负责让容器以 Pod 的模式运行。- 容器运行时:负责运行容器,例如 Docker、cri-o、containerd…
- 内核:工作节点上的操作系统内核,负责管理进程。
- 目标容器:组成 Pod 的容器,在工作节点上运行。
探索
客户端的活动
在缺省命名空间中创建一个 Pod:
1 | # kubectl run exec-test-nginx --image=nginx |
执行 sleep 5000
,来进行观察:
1 | # ps -ef |grep kubectl |
检查该进程的网络活动,会看到连接到 API Server 的通信(192.168.205.10.6443)
1 | $ netstat -atnv |grep 8507 |
再看看代码。kubectl
发起了一个包含 exec
子资源的 POST 请求:
1 | req := restClient.Post(). |
Master 上的活动
在 API Server 端当然也能观察到请求的情况:
1 | handler.go:143] kube-apiserver: POST "/api/v1/namespaces/default/pods/exec-test-nginx-6558988d5-fgxgg/exec" satisfied by gorestful with webservice /api/v1 |
HTTP 请求中包含了协议升级的请求,SPDY 允许在单个 TCP 连接上复用独立的 stdin/stdout/stderr/spdy-error 流。
API Server 收到请求,绑定到 PodExecOptions
:
1 | // PodExecOptions is the query options to a Pod's remote exec call |
为了执行必要的动作,API Server 需要知道联系地址:
1 | // ExecLocation returns the exec URL for a pod container. If opts.Container is blank |
当然这个端点是来自 Node:
1 | nodeName := types.NodeName(pod.Spec.NodeName) |
Kubelet 提供了一个端口,API Server 可以进行连接:
1 | // GetConnectionInfo retrieves connection info from the status of a Node API object. |
API Server to Kubelet Kubelet 开放的是一个 HTTPS 端点。缺省情况下 API Server 是不会验证 Kubelet 的服务证书的,这样这个连接就存在遭到中间人攻击的隐患,在不受信任的或者公开的网络上运行是不安全的。
现在,API Server 得到了端点地址,打开连接:
1 | // Connect returns a handler for the pod exec proxy |
看看 Master 上发生了什么。
首先确定一下工作节点的 IP,这里是 192.168.205.11
:
1 | $ kubectl get nodes k8s-node-1 -o wide |
然后查找 Kubelet 的端口号:
1 | $ kubectl get nodes k8s-node-1 -o jsonpath='{.status.daemonEndpoints.kubeletEndpoint}' |
接下来看看是不是存在到工作节点的连接?看到连接之后,如果杀掉 exec
进程,这个连接就会消失。这说明这个连接是 API Server 响应 exec
请求而生成的:
1 | $ netstat -atn |grep 192.168.205.11 |
目前为止,kubectl 和 API Server 之间的连接还存在,并且 API Server 和 Kubelet 之间也建立了连接。
工作节点上的活动
接下来我们连接到工作节点上,看看这里发生了什么。
首先我们同样能看到连接,第二行显示了 Master 的地址:192.168.205.10
。
1 | // worker node |
我们的 sleep 命令呢?也可以看到:
1 | // worker node |
Kubelet 是如何做到的?
Kubelet 提供了一个服务端口,用来响应 API Server 的请求:
1 | // Server is the library interface to serve the stream requests. |
Kubelet 为 exec 请求生成一个响应端点:
1 | func (s *server) GetExec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) { |
它返回的不是命令结果,而是一个用于通信的端点:
1 | type ExecResponse struct { |
Kubelet 实现了一个 CRI 规范中的 RuntimeServiceClient
接口:
1 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. |
使用 gRPC 通过 CRI 调用方法:
1 | type runtimeServiceClient struct { |
容器运行时负责实现 RuntimeServiceServer
:
1 | // RuntimeServiceServer is the server API for RuntimeService service. |
既然如此,我们就该看看 Kubelet 和容器运行时之间的连接。
1 | // worker node |
在 Kubelet(PID=5714)和 Docker 之间有一个新的 Unix Socket 连接:
1 | // worker node |
是 Docker 守护进程(PID 1186)执行了我们的命令:
1 | // worker node. |
容器运行时的活动
看看 cri-o 的源码,了解一下相关内容。运行逻辑和 Docker 类似。
它提供了一个服务,实现了 RuntimeServiceServer
:
1 | // Server implements the RuntimeService and ImageService |
链条的最后一环,容器运行时在工作节点上执行命令:
1 | // ExecContainer prepares a streaming endpoint to execute a command in the container. |
最后,内核执行了任务:
总结
- API Server 会向 Kubelet 发起连接。
- 在 exec 结束之前,连接持续存在。
- Kubectl 和 API Server 之间
- API Server 和 Kubelet 之间
- Kubelet 和容器运行时之间
- Kubectl 或者 API Server 无法在工作节点上运行任何东西。Kubelet 可以通过和容器运行时的互动来完成任务。